﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;

namespace MUD.Network
{
    /// <summary>
    /// Right, redoing the socket server entirely, since the MSDN example is structured completely awkward. 
    /// The client contacts the master socket on this server and is assigned a set of SocketAsyncEventArgs, 
    /// one for sending and the other to loop continuously and recieve. The client object must encapsulate 
    /// the methods for sending, while this server processes recieve events and fires the appropriate event.
    /// </summary>
    public class SocketServer
    {
        #region Fields
        /// <summary>
        /// Collection of client objects.
        /// </summary>
        public ClientCollection Clients = new ClientCollection();

        private uint _maxConnections;
        private uint _maxIncomingBufferSize;
        private Semaphore _maxAcceptedClients;
        private Socket _mainListener;
        private IPEndPoint _listenAt;
        

        #endregion

        #region Events

        /// <summary>
        /// Event for connection state changes.
        /// </summary>
        public event EventHandler<ConnectionEventArgs> OnConnectionStateChange;

        /// <summary>
        /// Event fired when data is received from a client.
        /// </summary>
        public event EventHandler<ReceiveEventArgs> OnReceive;

        /// <summary>
        /// Event fired when a log message is produced and needs to be captured.
        /// </summary>
        public event EventHandler<LogEventArgs> OnLog;

        #endregion

        #region Constructor(s) and Properties

        public SocketServer()
        {

        }

        #endregion

        #region Startup Methods

        /// <summary>
        /// Read settings from app.config and set up the server to be ready to start.
        /// </summary>
        public void Initialize(uint MaxUsers, uint MaxIncomingBufferSize, string ListenerHost, int ListenerPort)
        {
            // load and initialize everything.
            Log("SocketServer booting up...");
            _maxConnections = MaxUsers;
            _maxIncomingBufferSize = MaxIncomingBufferSize;
            _maxAcceptedClients = new Semaphore((int)_maxConnections, (int)_maxConnections);
            Log("Maximum connections set to " + _maxConnections);
            _listenAt = new IPEndPoint(IPAddress.Parse(ListenerHost), ListenerPort);
            _mainListener = new Socket(_listenAt.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _mainListener.Bind(_listenAt);
            Log("Server bound to " + _listenAt.Address.ToString() + " on port " + _listenAt.Port);
            Log("*** Ready to start accepting connections ***" + Environment.NewLine);
        }

        public void Start()
        {
            // start listening and accepting communications.
            Log("Starting the server!");
            _mainListener.Listen((int)_maxConnections);
            Log("Socket server beginning asynchronous accept loop");
            StartAccept(null);
        }

        #endregion

        #region Connection Management

        private void StartAccept(SocketAsyncEventArgs e)
        {
            // this is the first time we've started the server?
            if (e == null)
            {
                // set up a new args set and wire it up
                e = new SocketAsyncEventArgs();
                e.Completed += new EventHandler<SocketAsyncEventArgs>(Accept_Completed);
            }
            else
            {
                // ok, we just finished accepting a connection, so this socket is now in use, we need to be able to
                //   grab a new one.
                e.AcceptSocket = null;
            }

            // check the semaphore to make sure we don't have too many clients connected.
            _maxAcceptedClients.WaitOne();
            
            // fire the async accept method.
            if (!_mainListener.AcceptAsync(e))
            {
                ProcessAccept(e);
            }

        }

        private void Accept_Completed(object sender, SocketAsyncEventArgs e)
        {
            ProcessAccept(e);
        }

        private void ProcessAccept(SocketAsyncEventArgs e)
        {
            // grab a new client object, accept the incoming socket connection and add the client to the collection.
            AsyncClient newClient = new AsyncClient(e.AcceptSocket, (int)_maxIncomingBufferSize);
            newClient.RecieveArgs.Completed += new EventHandler<SocketAsyncEventArgs>(Receive_Completed);
            newClient.SendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(Send_Completed);
            newClient.OnSendSynchronous += new EventHandler(Send_Synchronous);
            newClient.OnDisconnectRequest += new EventHandler(newClient_OnDisconnectRequest);
            Clients.Add(newClient);

            // fire off a new connection event
            if (OnConnectionStateChange != null)
                OnConnectionStateChange(this, new ConnectionEventArgs(newClient, ConnectionEventType.Connected));

            Log("Client connected from " + ((IPEndPoint)newClient.Socket.RemoteEndPoint).Address.ToString() + ". Total Clients: " + Clients.Count);

            // Start listening on this socket. If the async call returns false, the operation was synchronous and we should
            //   call the process method immediately.
            if (!newClient.Socket.ReceiveAsync(newClient.RecieveArgs))
            {
                ProcessReceive(newClient.RecieveArgs);
            }

            StartAccept(e);
        }

        void newClient_OnDisconnectRequest(object sender, EventArgs e)
        {
            CloseConnection(((AsyncClient)sender).RecieveArgs);
        }

        void Send_Synchronous(object sender, EventArgs e)
        {
            ProcessSend(((AsyncClient)sender).SendArgs);
        }

        public void CloseConnection(SocketAsyncEventArgs e)
        {
            // grab the appropriate client object
            AsyncClient client = Clients[e];
            // remove it from the currently connected collection
            if (!Clients.Contains(client))
                return;
            Clients.Remove(client);
            // release the semaphore instance
            _maxAcceptedClients.Release();
            try
            {
                // shutdown the socket for this client
                client.Socket.Shutdown(SocketShutdown.Both);
            }
            catch (Exception ex)
            {
                if (OnLog != null)
                    OnLog(this, new LogEventArgs(ex.Message));
            }
            if (OnConnectionStateChange != null)
            {
                OnConnectionStateChange(this, new ConnectionEventArgs(client, ConnectionEventType.Disconnected));
            }
            Log("Client at " + ((IPEndPoint)client.Socket.RemoteEndPoint).Address.ToString() + " has disconnected. Total Clients: " + Clients.Count);

            // remove last reference to the client
            client = null;
        }

        #endregion

        #region Listener

        void Receive_Completed(object sender, SocketAsyncEventArgs e)
        {
            ProcessReceive(e);
        }

        void Send_Completed(object sender, SocketAsyncEventArgs e)
        {
            ProcessSend(e);
        }

        private void ProcessReceive(SocketAsyncEventArgs e)
        {
            // grab the right client for this args set.
            AsyncClient client = Clients[e];

            // if we transferred something and it was successful...
            if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
            {
                // decode the message
                string message = ASCIIEncoding.ASCII.GetString(Slice(e.Buffer, e.Offset, e.BytesTransferred));

                // fire a message recieved event
                if (OnReceive != null)
                    OnReceive(this, new ReceiveEventArgs(client, message));

                // keep listening
                if (!client.Socket.ReceiveAsync(client.RecieveArgs))
                {
                    ProcessReceive(client.RecieveArgs);
                }
            }
            else
            {
                // something happened, we need to disconnect this client.
                CloseConnection(e);
            }
        }

        private void ProcessSend(SocketAsyncEventArgs e)
        {
            // if the send result is anything but a success, close the connection
            if (e.SocketError != SocketError.Success)
            {
                CloseConnection(e);
            }
            else
            {
                AsyncClient client = Clients[((IPEndPoint)e.AcceptSocket.RemoteEndPoint).Address];
                if (client != null && client.HasBufferedMessages)
                    client.SendQueued();
            }
        }

        private void Log(string message)
        {
            if (OnLog != null)
            {
                OnLog(this, new LogEventArgs(message));
            }
        }

        #endregion

        #region Utility Methods

        public byte[] Slice(byte[] buffer, int offset, int length)
        {
            byte[] retval = new byte[length];
            for (int i = 0; i < length; i++)
            {
                retval[i] = buffer[i + offset];
            }
            return retval;
        }

        #endregion
    }
}
